/*
 * Copyright (C) 2011 : Gilles Coomans 
 * 
 * This file is part of QuintEssence.
 * QuintEssence is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * QuintEssence is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with QuintEssence.  If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * ...
 * ajout : keepStructure when applying something : juste collide
 * @author Gilles Coomans
 */
package net.exprom.qe;
import haxe.Timer;
import net.exprom.qe.fields.QEField;
import net.exprom.qe.fields.QELeaf;
import net.exprom.qe.fields.QENodeList;
import net.exprom.qe.fields.QENode;
import net.exprom.tools.TraceTools;
//import net.exprom.qe.IQELayerable;

enum QEHistoricItemType{ 
	QEHI_FIELD;
	QEHI_LAYER;
	QEHI_METAS_MODEL;
	QEHI_STACK;
	QEHI_REMOVE_FIELD;
}
 
typedef QEHistoricItem = 
{ 	
	type:QEHistoricItemType,
	fieldName:String,
	commonOnly:Bool,
	obj:Dynamic, // QEFT_LEAF : la valeur, LIST : l'array de ModelInstance
	up:Bool	// le field associé (son model)
};
 
class QEStack
{	
	private var evaluated:Bool;
	public var parent:QEStack;
	public var rootContext:QEStack;
	
	public var final:Bool;
	public var finalModel:Bool;
	
	public var namespace:String;
	public var path:String;
	public var name:String;
	public var fields:Hash<QEField>;
	public var historic:List<QEHistoricItem>;
	public var compiling:Bool;
	
	private var IS_QESTACK:Bool;
	public var IS_METAS:Bool;
	
	public function new(ns:String = "undefined_ns", name:String = "undefinedStack") 
	{
		IS_QESTACK = true;
		evaluated = false;
		fields = new Hash();
		historic = new List();
		compiling = false;
		this.namespace = ns;
		this.name = name;
		this.path = namespace + "." + name;
		IS_METAS = false;
		parent = null;
		rootContext = null;
		finalModel = false;
	}
	
	public function addLeaf(fieldName:String, param:Dynamic = null, up:Bool = true):Void
	{
		#if QEC_DEBUG message("add Leaf field : " + fieldName); #end
		if (final  || finalModel)
			throw "QEStack " + path + " is final ! You could'nt add anymore field (leaf or node) to this model.";
		if(!historic.isEmpty())
			historic.add( { type:QEHI_FIELD, fieldName:fieldName, up:up, obj: { params:param, type:QEFT_LEAF }, commonOnly:false } );
		else{
			var leaf:QELeaf = new QELeaf(fieldName, this, param);
			addField(leaf);
		}
	}
		
	public function addNode(fieldName:String, param:Dynamic = null, up:Bool = true):Void
	{
		#if QEC_DEBUG message("add MoldelNode field : " + fieldName); #end
		if (final)
			throw "QEStack " + path + " is final ! You could'nt add anymore field (leaf or node) to this model.";
		if(!historic.isEmpty())
			historic.add( { type:QEHI_FIELD, fieldName:fieldName, up:up, obj: { params:param, type:QEFT_NODE }, commonOnly:false } );
		else{
			var node:QENode = new QENode(fieldName, this, param);
			addField(node);
		}
	}
	
	public function addNodeList(fieldName:String, param:Dynamic = null, up:Bool = true):Void
	{
		#if QEC_DEBUG message("add ModelList field : " + fieldName); #end
		if (final)
			throw "QEStack " + path + " is final ! You could'nt add anymore field (leaf or node) to this object";
		if(!historic.isEmpty())
			historic.add( { type:QEHI_FIELD, fieldName:fieldName, up:up, obj: { params:param, type:QEFT_NODE_LIST }, commonOnly:false } );
		else
		{
			var node:QENodeList = new QENodeList(fieldName, this, param);
			addField(node);
		}	
	}
	
	//.....
	public function addMetasField(fieldName:String, params:Dynamic, up:Bool = true):Void
	{
		#if QEC_DEBUG message("add Metas_Only field : " + fieldName + " - with : " + params.model); #end
		if (final)
			throw "QEStack " + path + " is final ! You could'nt add anymore field (leaf or node) to this object";
		if(!historic.isEmpty())
			historic.add( { type:QEHI_FIELD, fieldName:fieldName, up:up, obj: { params:params, type:QEFT_METAS_ONLY }, commonOnly:false } );
		else
		{
			var f = new QEField(fieldName, this, params);
			f.parameters.visibility = QEFV_METAS_ONLY;
			addField(f);
		}
	}
	
	private function addField(f:QEField):Void
	{
		#if QEC_DEBUG message("add field : " + name); #end
		if (final)
			throw "QEStack " + path + " is final ! You could'nt add anymore field (leaf or node) to this model.";
		fields.set(f.name, f);
		f.setParent (this);
		if (rootContext != null)
			f.rootContext = this.rootContext;
		else
			f.rootContext = this;
		f.setNamespace(path);
	}
	
	public function resetLeafs():Void
	{
		for (f in fields)
			f.resetLeafs();
	}
	
	public function applyStack(st:QEStack, up:Bool = true, commonOnly:Bool = false):Void
	{
		historic.add({type:QEHI_STACK, obj:st, fieldName:null, up:up, commonOnly:commonOnly});	
	}
	
	public function applyMetasModel(fieldName:String, model:Dynamic, up:Bool = true, commonOnly:Bool = false):Void
	{
		historic.add({type:QEHI_METAS_MODEL, obj:model, fieldName:fieldName, up:up, commonOnly:commonOnly});	
	}
	
	public function applyLayer(layer:Dynamic, up:Bool = true, commonOnly:Bool = false):Void
	{
		historic.add({type:QEHI_LAYER, obj:layer, up:up, fieldName:null, commonOnly:commonOnly});
	}
	
	public function removeField(fieldName:String):Void
	{
		if (historic.isEmpty())
			fields.remove(fieldName);
		else
			historic.add({type:QEHI_REMOVE_FIELD, fieldName:fieldName, up:true, obj:null, commonOnly:false});
	}
	//
		
	private function resolveItemObject(item:Dynamic):QEStack 
	{	
		#if QEC_DEBUG message("try resolving : " + item.obj); #end
		if (Std.is(item.obj, String))
		{
			#if QEC_DEBUG message("will resolve: " + item.obj); #end
			var obj = QECore.resolve(item.obj);
			if (obj == null)
				throw "Stack Compilation Error ( path : " + path + ") : while resolving reference " + item.obj; 
			return obj;
		}
		return item.obj;
	}
	
	public function compil():Void
	{	
		if (compiling || historic.isEmpty())
			return;
		#if QEC_DEBUG message("compil()"); #end
		compiling = true;
		compilHistoric(historic);
		historic.clear();
		compiling = false;
		#if QEC_DEBUG message("compil() - END"); #end
	}
	
	private function compilFieldHistoricItem(item:QEHistoricItem, commonOnly:Bool = false):Void
	{
		var fieldName:String = item.fieldName;
		var commOnly:Bool = item.commonOnly || commonOnly;
		var up:Bool = item.up;
		var params = item.obj.params;
		var f:QEField = fields.get(fieldName);
		
		if (f != null)
		{
			f.applyParameters(params, up, commOnly);
		}
		else if (commOnly)
			return;
		
		var type:QEFieldType = item.obj.type;
		switch(type)
		{
			case QEFT_METAS_ONLY : 
				f = new QEField(fieldName, this, params);
				f.parameters.visibility = QEFV_METAS_ONLY;
			case QEFT_NODE_LIST :
				f = new QENodeList(fieldName, this, params);
			case QEFT_LEAF :
				f = new QELeaf(fieldName, this, params);
			case QEFT_NODE :
				f = new QENode(fieldName, this, params);
		}
		addField(f);
	}
	
	private function compilHistoric(hist:List<QEHistoricItem>, commonOnly:Bool = false):Void
	{
		for(item in hist)
		{
			var commOnly:Bool = item.commonOnly || commonOnly;
			switch(item.type)
			{
				case QEHI_FIELD : 
					compilFieldHistoricItem(item, commOnly);
				case QEHI_STACK : 
					var stack:Dynamic = resolveItemObject(item);
					#if QEC_DEBUG message("compil() : will apply stack : " + stack.path); #end
					compilStack(stack, item.up, commOnly);
				case QEHI_LAYER : 
					var layer = resolveItemObject(item);
					#if QEC_DEBUG message("compil() : will apply layer"); #end
					compilLayer(layer, item.up, commOnly);
				case QEHI_METAS_MODEL : 
					var metasModel:QEStack = resolveItemObject(item);
					#if QEC_DEBUG message("compil() : will apply metas model : " + metasModel.path); #end
					var f:QEField = fields.get(item.fieldName);
					if (f == null)
					{
						if(!commOnly)
							throw "QEStack Compilation Error : " + path + " : compilMetasModel() failed : no field found with : " + item.fieldName;
					}
					else
						f.applyMetasModel(metasModel, item.up, commOnly );
				case QEHI_REMOVE_FIELD : 
					fields.remove(item.fieldName);
			}
		}
	}
	
	public function compilStack(stack:QEStack, up:Bool = true, commonOnly:Bool = false):Void
	{
		#if QEC_DEBUG message("compilStack() : "+stack.path); #end
		if (stack == this)
			return;
		for(cf in stack.fields)
		{
			var field = fields.get(cf.name);
			if(field != null) // collision
			{
				#if QEC_DEBUG message("compilStack() : field collision " + cf.name); #end
				field.compilField(cf, up, commonOnly); // place dans l'historique model 
			}
			else if(!commonOnly)
			{
				#if QEC_DEBUG message("compilStack() : adding field " + cf.name); #end
				field = cf.clone();
				addField(field);
			}
		}
		if(!stack.historic.isEmpty())
			compilHistoric(stack.historic, commonOnly);
		#if QEC_DEBUG message("compilStack() - END"); #end
	}
	
	
	private inline function compil_PARAMSLayer(layer:Dynamic, up:Bool = true, commonOnly:Bool = false):Void
	{
		for (fmk in Reflect.fields(layer))
		{
			#if QEC_DEBUG message("applyMetasLayer on " + fmk); #end
			var val = Reflect.field(layer, fmk);
			var field:QEField = fields.get(fmk);
			if (field == null)
			{
				if (commonOnly)
					continue;
				var ft:QEFieldType = Type.createEnum(QEFieldType, Reflect.field(val, "type"));
				if (ft == null)
					throw "As no field of this name ("+fmk+") is already in stack ("+path+"), field's type must be provided in _PARAMS layer to proceed.";
				compilFieldHistoricItem( { type:QEHI_FIELD, fieldName:fmk, up:up, commonOnly:false, obj:{ params:val, type:ft } } );
			}
			else
				field.applyParameters(val, up, commonOnly);
		}
	}
	
	private inline function compil_METASLayer(layer:Dynamic, up:Bool = true, commonOnly:Bool = false):Void
	{
		for (fmk in Reflect.fields(layer))
		{
			#if QEC_DEBUG message("applyMetasLayer on " + fmk); #end
			var val = Reflect.field(layer, fmk);
			var field:QEField = fields.get(fmk);
			if (field == null)
			{
				if(!commonOnly)
					throw "Stack Compilation Error ( "+path+" ) : compiling layer's _METAS : unrecognised field : " + fmk;
			}
			else
			{
				if (!field.setted)
					field.setted = true;
				field.applyMetasLayer(val, up, commonOnly);
			}
		}
	}
		
	private function compilLayer(layer:Dynamic, up:Bool = true, commonOnly:Bool = false)
	{
		#if QEC_DEBUG message("compilLayer()"); #end
		var field:QEField = null;
		var setted = null;
		var val = null;
		for (fk in Reflect.fields(layer))
		{
			#if QEC_DEBUG message("compilLayer() : found layer field : "+fk); #end
			if(fk == "_METAS")
			{	
				compil_METASLayer(layer._METAS, up, commonOnly);
				continue;
			}
			if(fk == "_PARAMS")
			{	
				compil_PARAMSLayer(layer._PARAMS, up, commonOnly);
				continue;
			}
			#if QEC_DEBUG message("applyLayer on " + fk); #end
			field = fields.get(fk);
			if (field == null)
			{
				if(commonOnly)
					continue;
				throw "Stack Compilation Error ( " + path + " ) : compilLayer : unrecognised field : " + fk;
			}
			val = Reflect.field(layer, fk); 
			switch(field.fieldType)
			{
				case QEFT_LEAF :
					#if QEC_DEBUG message("have setted leaf : " + field.name + " : " + val); #end
					untyped field.setValue(val);
				default : 
					#if QEC_DEBUG message("have setted field : " + field.name + " : " + field.fieldType); #end
					field.applyLayer(val, up, commonOnly);
					field.setted = true;
			}	
		}
		#if QEC_DEBUG message("compilLayer() : End"); #end
	}
	
	public function finalStruct(includeMetas:Bool = false, fixEvaluation:Bool = false, validateLeafs:Bool = false, includeDefaults:Bool = false):Dynamic
	{
		#if QEC_DEBUG message("finalStruct()"); #end
		//var time = Timer.stamp();
		//TraceTools.tabsLevel++;
		//TraceTools.tabbedTrace("QEStack.finalStruct() : ( parentModel : " + ((parentModel != null)?parentModel.path:"null") + ")");
		if (!historic.isEmpty())
			compil();	
		//if (fixEvaluation)
			//fixEvaluables();
		if (validateLeafs)
			validate();
		var o = { };
		Reflect.setField(o, "_METAS", {});
		var metasSetted:Bool = false;
		var haveSetteds = false;
		//var count = 0;
		//TraceTools.tabsLevel++;
		for(field in fields)
		{
			field.compil();
			//TraceTools.tabbedTrace("QEStack : " + ((parentModel != null)?parentModel.path:"null") + " : produce finalStruct : for field : " + setted.path);
			//TraceTools.tabbedTrace("QEStack : setted : "+setted.path);
			var nodeVal = field.finalStruct(includeMetas, fixEvaluation, validateLeafs, includeDefaults);
			//var setted = setteds.get(field.name);
			switch(field.fieldType)
			{
				case QEFT_LEAF : 
					var finalVal:Dynamic = null;
					if (fixEvaluation  && field.setted && untyped f.evaluable == true)
						finalVal = untyped field.value(true);
					if (finalVal == null)
					{
						if (field.setted) 
							finalVal = untyped field._value;
						else if (includeDefaults)
							finalVal = untyped field.parameters.defaultValue;
					}
					if(finalVal != null)
					{
						haveSetteds = true;
						Reflect.setField(o, field.name, finalVal);
					}
				default : 
					if(Reflect.hasField(nodeVal, field.name) && Reflect.field(nodeVal, field.name) != null)
					{
						haveSetteds = true;
						Reflect.setField(o, field.name, Reflect.field(nodeVal, field.name));
					}
			}
			if (includeMetas && Reflect.hasField(nodeVal._METAS, field.name) && Reflect.field(nodeVal._METAS, field.name) != null) 
			{
				haveSetteds = true;
				metasSetted = true;
				untyped Reflect.setField(o._METAS, field.name, Reflect.field(nodeVal._METAS, field.name));
			}
			//TraceTools.tabbedTrace("out");
			//count++;
		}
		//TraceTools.tabsLevel--;
		//TraceTools.tabbedTrace("END - QEStack.finalStruct() : ( parentModel : " + ((parentModel != null)?parentModel.path:"null") + " ) - number of setteds : " + count);
		//message("finalStruct() in (ms) " + ((Timer.stamp() - time) * 1000));
		//TraceTools.tabsLevel--;
		if (!metasSetted)
			Reflect.deleteField(o, "_METAS");
 		if (!haveSetteds)
			return null;
		return o;
	}
	
	public function clone():Dynamic
	{
		#if QEC_DEBUG message("clone()"); #end
		var ns:QEStack = new QEStack();
		for(cf in fields)
		{
			var field:QEField = cf.clone();
			field.setParent(ns);
			field.rootContext = null;
			field.setNamespace("UndefinedQEStack");
			ns.fields.set(cf.name, field);
			if(cf.setted)
			{
				if(cf.fieldType == QEFT_LEAF)
					untyped field.setValue(cf._value);
				field.setted = true;
			}
		}
		for(hi in historic)
			ns.historic.add(hi);	
		return ns;
	}
	
	public function setParent(parent:QEStack):Void
	{
		this.parent = parent;
	}
	
	public function fixEvaluables():Void
	{
		if (evaluated)
			return;
		if (!historic.isEmpty())
			compil();
		for (f in fields)
			f.fixEvaluables();
		evaluated = true;
	}
	
	public function setNamespace(namespace:String, name:String = null):Void
	{
		if(name != null)
			this.name = name;
		this.namespace = namespace;
		path = namespace + "." + this.name;
		for (f in fields)
			f.setNamespace(path);
	}
	
	private function message(msg:String, app:String = null, ref:Dynamic = null)
	{
		var r = "QEStack : " + path + " : ";
		if (app != null)
			r += " : " + app;
		if (ref != null)
		{
			if (Std.is(ref, String))
				r += " : " + ref;
			else
				r += " : " + ref.path;
		}
		r += " : " +msg;
		trace(r);
	}
	
	public inline function getField(fieldName:String):Dynamic
	{
		return fields.get(fieldName);
	}
	
	public function validate():Bool
	{
		if(!historic.isEmpty())
			compil();
		var ok = true;
		for (f in fields)
			ok = ok && f.validate();
		return ok;
	}
}